﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Xml.Linq;
using System.Data.Metadata.Edm;
using System.Data.Common;
using System.IO;
using System.Xml;
using System.Data.Objects.DataClasses;
using System.Data.Objects;

namespace EFMassOperations
{
	class EFMetadataUtils
	{
		public static string GetTableName<TObj>(ObjectSet<TObj> objectSet)
			where TObj : EntityObject
		{
			string storeContainerName, storeSetName;
			IEnumerable<XElement> propertyMapping;
			ReadMappingMetadata(objectSet,
				out storeContainerName, out storeSetName, out propertyMapping);

			EntityContainer storeEntityContainer = objectSet.Context.MetadataWorkspace
				.GetItem<EntityContainer>(storeContainerName, false, DataSpace.SSpace);
			EntitySet storeEntitySet = storeEntityContainer.GetEntitySetByName(storeSetName, false);
			string tableName, schemaName = null, tableNameQuoted, schemaNameQuoted = null;
			tableName = storeEntitySet.Name;
			MetadataProperty schemaProperty = storeEntitySet.MetadataProperties.Where(prop => prop.Name == "Schema").SingleOrDefault();
			if (schemaProperty != null)
				schemaName = (string)schemaProperty.Value;

			tableNameQuoted = QuoteIdentifier(objectSet.Context, tableName);
			if (schemaName != null)
				schemaNameQuoted = QuoteIdentifier(objectSet.Context, schemaName);

			if (schemaNameQuoted != null)
				return schemaNameQuoted
					+ GetSchemaSeparator(objectSet.Context)
					+ tableNameQuoted;
			return tableNameQuoted;
		}

		public static IEnumerable<string>
			GetColumnNames<TObj>(ObjectSet<TObj> objectSet, IEnumerable<string> properties)
			where TObj : EntityObject
		{
			string storeContainerName, storeSetName;
			IEnumerable<XElement> propertyMapping;
			ReadMappingMetadata(objectSet,
				out storeContainerName, out storeSetName, out propertyMapping);

			IEnumerable<string> columns =
			properties.Select((propName) => propertyMapping.Single(
				mapping => mapping.Attribute("Name").Value == propName
				).Attribute("ColumnName").Value);

			return columns.Select(col=>QuoteIdentifier(objectSet.Context, col));
		}

		static void ReadMappingMetadata<TObj>(ObjectSet<TObj> objectSet,
			out string storeContainerName, out string storeSetName,
			out IEnumerable<XElement> propertyMapping)
			where TObj : EntityObject
		{
			string conceptualContainerName = objectSet.EntitySet.EntityContainer.Name;
			string conceptualSetName = objectSet.EntitySet.Name;
			XDocument metadata = GetMappingMetadataCache(objectSet.Context);
			ReadMappingMetadata(metadata, conceptualContainerName, conceptualSetName,
				out storeContainerName, out storeSetName, out propertyMapping);
		}

		static void ReadMappingMetadata(XDocument metadata,
			string conceptualContainerName, string conceptualSetName,
			out string storeContainerName, out string storeSetName,
			out IEnumerable<XElement> propertyMapping)
		{
			XNamespace ns = metadata.Root.GetDefaultNamespace();
			var container = metadata.Root.Elements(ns + "EntityContainerMapping")
				.Where(e => e.Attribute("CdmEntityContainer").Value == conceptualContainerName).Single();
			storeContainerName = container.Attribute("StorageEntityContainer").Value;
			IEnumerable<XElement> fragmentsQuery = container
				.Elements(ns + "EntitySetMapping")
				.Where(e => e.Attribute("Name").Value == conceptualSetName)
				.Single()
				.Descendants(ns + "MappingFragment");
			XElement fragment;
			try
			{
				fragment = fragmentsQuery.Single();
			}
			catch (InvalidOperationException ex)
			{
				throw new InvalidOperationException("Entities with only one mapping fragment are supported", ex);
			}
			storeSetName = fragment.Attribute("StoreEntitySet").Value;
			propertyMapping = fragment.Elements(ns + "ScalarProperty");
		}

		static string QuoteIdentifier(ObjectContext context, string identifier)
		{
			DbCommandBuilder commandBuilder =
				EFDbProviderFactoryUtils.GetSqlCommandBuilderCache(context);
			return commandBuilder.QuoteIdentifier(identifier);
		}

		static string GetSchemaSeparator(ObjectContext context)
		{
			DbCommandBuilder commandBuilder =
				EFDbProviderFactoryUtils.GetSqlCommandBuilderCache(context);
			return commandBuilder.SchemaSeparator;
		}

		// saves a lot of processing time
		static Cache<ObjectContext, string, XDocument> metadataCache =
			new Cache<ObjectContext, string, XDocument>(
				(context) => context.Connection.ConnectionString,
				(context, connectionString) => GetMappingMetadata(context));

		static XDocument GetMappingMetadataCache(ObjectContext context)
		{
			return metadataCache[context];
		}

		static XDocument GetMappingMetadata(ObjectContext context)
		{
			const string pfxRes = "res://*/";
			string[] metadata = GetMetadataURIs(context);
			string mslUri = metadata.Where(s => s.EndsWith(".msl", StringComparison.InvariantCultureIgnoreCase)).Single();
			Stream mslStream;
			XDocument doc;

			if (mslUri.StartsWith(pfxRes))
			{
				string resName = mslUri.Substring(pfxRes.Length);
				mslStream = context.GetType().Assembly.GetManifestResourceStream(resName);
			}
			else // since EF does not support file:// scheme, assume everyting else is a file path
			{
				string fileName = mslUri;
				mslStream = new FileStream(fileName, FileMode.Open, FileAccess.Read);
			}
			using (mslStream)
			using (XmlTextReader xmlReader = new XmlTextReader(mslStream))
			{
				doc = XDocument.Load(xmlReader);
			}

			return doc;
		}

		static string[] GetMetadataURIs(ObjectContext context)
		{
			System.Data.EntityClient.EntityConnectionStringBuilder parser =
				EFConnectionStringParser.GetConnectionStringParserCache(context);
			string[] metadata = parser.Metadata.Split('|');
			return metadata;
		}
	}
}
